<?php

/*
 * Copyright 2012, Serge V. Izmaylov
 * Released under GPL Version 2 license.
 */

require_once("jsonss.php");

function __jsonsscli_curl_read_func($ch, $fd, $len) {
    if ($len >= strlen(JSONSSProxy::$__rawpostdata)) {
        $tmp = JSONSSProxy::$__rawpostdata;
        JSONSSProxy::$__rawpostdata = "";
        return $tmp;
    } else {
        $tmp = substr(JSONSSProxy::$__rawpostdata, 0, $len);
        JSONSSProxy::$__rawpostdata = substr(JSONSSProxy::$__rawpostdata, $len);
        return $tmp;
    }
}

class JSONSSProxy {
    public $serviceUrl = "";
    public $lastStatus = "";
    public $lastRequestHeaders = "";
    public $lastRequest = "";
    public $lastResponseHeaders = "";
    public $lastResponse = "";
    public static $__rawpostdata = "";

    private $__constructing = true;
    private $__cookie = "";
    private $__members = "";
    private static $__cookiefile;
    private static $__curlch;

    public static function initLibrary() {
        self::$__cookiefile = tempnam(sys_get_temp_dir(), "jsonssck");
        self::$__curlch = curl_init();
        curl_setopt(self::$__curlch, CURLOPT_NOSIGNAL, true);
        curl_setopt(self::$__curlch, CURLOPT_TIMEOUT, 15);
        curl_setopt(self::$__curlch, CURLOPT_HEADER, true);
        curl_setopt(self::$__curlch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt(self::$__curlch, CURLOPT_COOKIEFILE, JSONSSProxy::$__cookiefile);
        curl_setopt(self::$__curlch, CURLOPT_COOKIEJAR, JSONSSProxy::$__cookiefile);
    }

    public static function shutdownLibrary() {
        curl_close(self::$__curlch);
        unlink(self::$__cookiefile);
    }

    public static function fromURL($url) {
        $resp = self::__query_service($url, "GET");
        return self::fromInterface(json_decode($resp->responseText), $url);
    }

    public static function fromText($json, $url) {
        return JSONSSProxy::fromInterface(json_decode($json), $url);
    }

    public static function fromInterface($intf, $url) {
        return new JSONSSProxy($intf, $url);
    }

    protected function __construct($intf, $url) {
        if(!is_object($intf))
            throw new JSONSSError(501, "JSONSSProxy needs interface description object to construct");
        if(!is_string($url))
            throw new JSONSSError(501, "JSONSSProxy needs URL to construct");
        $this->serviceUrl = $url;
        $this->__buildInterface($intf);
        $this->__constructing = false;
    }

    private function __buildInterface($intf) {
        if (!is_object($intf))
            throw new JSONSSError(501, "Bad interface object", $intf);
        if (!isset($intf->t) || (($intf->t != JSONSS_INTERFACE) && ($intf->t != JSONSS_SERVICE)))
            throw new JSONSSError(501, "Interface format error", $intf);
        if (!isset($intf->i) || !is_array($intf->i))
            throw new JSONSSError(501, "Interface memebers not found", $intf);
        for ($i = 0; $i < count($intf->i); $i++) {
            if (!is_object($intf->i[$i]) || !isset($intf->i[$i]->t) || !isset($intf->i[$i]->n) || 
                !is_string($intf->i[$i]->n) || !isset($intf->i[$i]->u) || !is_string($intf->i[$i]->u))
                throw new JSONSSError(501, "Bad interface member format", $intf[$i]);
            switch ($intf->i[$i]->t) {
                case JSONSS_SERVICE:
                    if (!isset($intf->i[$i]->i) || !is_array($intf->i[$i]->i))
                        throw new JSONSSError(501, "Service interface memebers not found", $intf->i[$i]);
                    $this->__members[$intf->i[$i]->n] = $intf->i[$i];
                    $tmpintf = new stdClass();
                    $tmpintf->t = JSONSS_INTERFACE;
                    $tmpintf->i = $intf->i[$i]->i;
                    $this->{$intf->i[$i]->n} = new JSONSSProxy($tmpintf, $this->serviceUrl . "/" . $intf->i[$i]->u);
                    break;
                case JSONSS_METHOD:
                    $this->__members[$intf->i[$i]->n] = $intf->i[$i];
                    break;
                case JSONSS_PROPERTY:
                    $this->__members[$intf->i[$i]->n] = $intf->i[$i];
                    break;
                case JSONSS_EVENT:
                    $this->__members[$intf->i[$i]->n] = $intf->i[$i];
                    break;
                default:
                    throw new JSONSSError(501, "Unknown type of interface member", $intf->i[$i]);
            }
        }
    }

    private function __get($n) {
        if (isset($this->__members[$n]) && isset($this->__members[$n]->t) &&
            ($this->__members[$n]->t == JSONSS_PROPERTY)) {
            if (!isset($this->__members[$n]->u) || empty($this->__members[$n]->u))
                throw new JSONSSError(501, "No urlname for property: " . $n);
            // XXX
            $url = $this->serviceUrl . "/" . $this->__members[$n]->u;
            $hdr = array(
                "Content-type: text/plain; charset=utf-8",
                "JSONSSMethod: " . JSONSS_METHOD_GET
            );
            $post = "";
            $this->lastRequest = $post;
            $this->lastRequestHeaders = implode("\r\n", $hdr) . "\r\n";
            $res = self::__query_service($url, "POST", $post, $hdr);
            if (!is_object($res) || !isset($res->responseText) || !isset($res->responseHeaders) || !isset($res->statusCode))
                throw new JSONSSError(501, "Internal error: invalid HTTP response format");
            $this->lastResponse = $res->responseText;
            $this->lastResponseHeaders = $res->responseHeaders;
            $this->lastStatus = $res->statusCode;
            return self::__parse_response($res->responseText);
        } elseif ((strlen($n) > 2) && (strncmp("on", $n, 2) == 0) &&
                  isset($this->__members[substr($n, 2)]) && ($this->__members[substr($n, 2)]->t == JSONSS_EVENT) &&
                  isset($this->__members[substr($n, 2)]->h)) {
            return $this->__members[substr($n, 2)]->h;
        } else {
            throw new JSONSSError(501, "Undefined property: " . $n);
        }
    }

    private function __set($n, $v) {
        if ($this->__constructing) {
            $this->$n = $v;
            return $v;
        }
        if (isset($this->__members[$n]) && isset($this->__members[$n]->t) &&
            ($this->__members[$n]->t == JSONSS_PROPERTY)) { // put service property
            if (!isset($this->__members[$n]->u) || empty($this->__members[$n]->u))
                throw new JSONSSError(501, "No urlname for property: " . $n);
            $url = $this->serviceUrl . "/" . $this->__members[$n]->u;
            $hdr = array(
                "Content-type: text/plain; charset=utf-8",
                "JSONSSMethod: " . JSONSS_METHOD_SET
            );
            $post = json_encode($v);
            $this->lastRequest = $post;
            $this->lastRequestHeaders = implode("\r\n", $hdr) . "\r\n";
            $res = self::__query_service($url, "POST", $post, $hdr);
            if (!is_object($res) || !isset($res->responseText) || !isset($res->responseHeaders) || !isset($res->statusCode))
                throw new JSONSSError(501, "Internal error: invalid HTTP response format");
            $this->lastResponse = $res->responseText;
            $this->lastResponseHeaders = $res->responseHeaders;
            $this->lastStatus = $res->statusCode;
            self::__parse_response($res->responseText);
            return  $v;
        } elseif ((strlen($n) > 2) && (strncmp("on", $n, 2) == 0) &&
                  isset($this->__members[substr($n, 2)]) && isset($this->__members[substr($n, 2)]->t) &&
                  ($this->__members[substr($n, 2)]->t == JSONSS_EVENT)) { // set up event handler
            $en = substr($n, 2);
            if (empty($v)) {
                $this->__members[$en]->h = NULL;
            } elseif (is_string($v)) {
                if (empty($v))
                    throw new JSONSSError(501, "Empty event handler function name");
                $this->__members[$en]->h = $v;
            } elseif (is_array($v)) {
                if (count($v) != 2 || !is_object($v[0]) || !is_string($v[1]) || empty($v[1]))
                    throw new JSONSSError(501, "Invalid event handler method format");
                $this->__members[$en]->h = $v;
            } else {
                throw new JSONSSError(501, "Event handler must be string or array");
            }
            return $v;
        } else {
            $this->$n = $v;
            return $v;
        }
    }

    private function __isset($n) {
        if (isset($this->__members[$n]) && isset($this->__members[$n]->t) &&
            ($this->__members[$n]->t == JSONSS_PROPERTY))
            return true;
        if ((strlen($n) > 2) && (strncmp("on", $n, 2) == 0) &&
            isset($this->__members[substr($n, 2)]) && isset($this->__members[substr($n, 2)]->t) &&
            ($this->__members[substr($n, 2)]->t == JSONSS_EVENT) &&
            isset($this->__members[substr($n, 2)]->h) && !empty($this->__members[substr($n, 2)]->h))
            return true;
        return false;
        
    }

    private function __unset($n) {
        if (isset($this->__members[$n]))
            throw new JSONSSError(501, "Can't unset interface item: " . $n);
    }

    private function __call($n, $a) {
        if (!isset($this->__members[$n]) || ($this->__members[$n]->t != JSONSS_METHOD))
            throw new JSONSSError(501, "Undefined method: " . $n);
        if (!isset($this->__members[$n]->u) || empty($this->__members[$n]->u))
            throw new JSONSSError(501, "No urlname for method: " . $n);
        $url = $this->serviceUrl . "/" . $this->__members[$n]->u;
        $hdr = array(
            "Content-type: text/plain; charset=utf-8",
            "JSONSSMethod: " . JSONSS_METHOD_CALL
        );

        if (!isset($a) || !is_array($a) || !count($a))
            $post = "";
        else
            $post = json_encode($a[0]);

        $this->lastRequest = $post;
        $this->lastRequestHeaders = implode("\r\n", $hdr) . "\r\n";
        $res = self::__query_service($url, "POST", $post, $hdr);
        if (!is_object($res) || !isset($res->responseText) || !isset($res->responseHeaders) || !isset($res->statusCode))
            throw new JSONSSError(501, "Internal error: invalid HTTP response format");
        $this->lastResponse = $res->responseText;
        $this->lastResponseHeaders = $res->responseHeaders;
        $this->lastStatus = $res->statusCode;
        return self::__parse_response($res->responseText);
    }

    public function ping() {
        $url = $this->serviceUrl;
        $hdr = array(
            "Content-type: text/plain; charset=utf-8",
            "JSONSSMethod: " . JSONSS_METHOD_PING
        );
        $post = "";
        $this->lastRequest = $post;
        $this->lastRequestHeaders = implode("\r\n", $hdr) . "\r\n";
        $res = self::__query_service($url, "POST", $post, $hdr);
        if (!is_object($res) || !isset($res->responseText) || !isset($res->responseHeaders) || !isset($res->statusCode))
            throw new JSONSSError(501, "Internal error: invalid HTTP response format");
        $this->lastResponse = $res->responseText;
        $this->lastResponseHeaders = $res->responseHeaders;
        $this->lastStatus = $res->statusCode;
        self::__parse_response($res->responseText);
    }

    private static function __query_service($url, $meth, $data = NULL, $headers = NULL) {
        if (!isset($url) || !isset($meth))
            throw new JSONSSError(501, "HTTP needs URL and method");
        curl_setopt(self::$__curlch, CURLOPT_URL, $url);
        if (isset($headers) && is_array($headers))
            curl_setopt(self::$__curlch, CURLOPT_HTTPHEADER, $headers);
        switch ($meth) {
            case "GET":
                curl_setopt(self::$__curlch, CURLOPT_HTTPGET, true);
                break;
            case "POST":
                curl_setopt(self::$__curlch, CURLOPT_POST, true);
                curl_setopt(self::$__curlch, CURLOPT_POSTFIELDS, "");
                if (!empty($data)) {
                    curl_setopt(self::$__curlch, CURLOPT_UPLOAD, true);
                    curl_setopt(self::$__curlch, CURLOPT_INFILE, STDIN);
                    curl_setopt(self::$__curlch, CURLOPT_INFILESIZE, strlen($data));
                    curl_setopt(self::$__curlch, CURLOPT_READFUNCTION, "__jsonsscli_curl_read_func");
                    curl_setopt(self::$__curlch, CURLOPT_CUSTOMREQUEST, "POST");
                    self::$__rawpostdata = $data;
                }
                break;
            default:
                throw new JSONSSError(501, "Unknown HTTP method: " . $meth);
        }
        $data = curl_exec(self::$__curlch);
        $hdrlen = curl_getinfo(self::$__curlch, CURLINFO_HEADER_SIZE);
        $res = new stdClass();
        $res->responseHeaders = substr($data, 0, $hdrlen);
        $res->responseText = substr($data, $hdrlen);
        $res->statusCode = curl_getinfo(self::$__curlch, CURLINFO_HTTP_CODE);
        return $res;
    }

    private function __parse_response($json) {
        if (!is_string($json))
            throw new JSONSSError(501, "Response text must be JSON string");
        $res = json_decode($json);
        if (!is_object($res) || !isset($res->t) || (($res->t != JSONSS_RESPONSE) && ($res->t != JSONSS_FAULT)))
            throw new JSONSSError(501, "Response format error");
        if ($res->t == JSONSS_FAULT) {
            $code = 501;
            $mesg = "Unknown service fault";
            $data = NULL;
            if (isset($res->d) && is_object($res->d)) {
                if (isset($res->d->c))
                    $code = (int)($res->d->c);
                if (isset($res->d->m))
                    $mesg = (string)($res->d->m);
                if (isset($res->d->d))
                    $data = $res->d->d;
            }
            throw new JSONSSError($code, $mesg, $data);
        }
        if (!isset($res->r))
            $ret = NULL;
        else
            $ret = $res->r;
        if (isset($res->e) && is_array($res->e))
            for ($i = 0; $i < count($res->e); $i++)
               if (is_object($res->e[$i]) && isset($res->e[$i]->n) && is_string($res->e[$i]->n)) {
                   $en = $res->e[$i]->n;
                   if (!isset($res->e[$i]->d))
                       $ed = NULL;
                   else
                       $ed = $res->e[$i]->d;
                   if (isset($this->__members[$en]) && isset($this->__members[$en]->t) &&
                       ($this->__members[$en]->t == JSONSS_EVENT) && isset($this->__members[$en]->h)) {
                       try {
                           call_user_func($this->__members[$en]->h, $ed);
                       } catch (Exception $x) {
                           throw self::__strip_error($x);
                       }
                   }
               }
        return $ret;
    }

    private static function __strip_error($x) {
        $res = new stdClass();
        if (!is_a($x, "JSONSSError")) {
            $res = $x;
        } else {
            $res = new JSONSSError((int)($x->getCode()), $x->getMessage(), isset($x->data)?$x->data:NULL);
        }
        return $res;
    }
}